package bancosys.tec.services.implementor.descriptor;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jmine.tec.di.injector.impl.MethodBasedInjector;
import jmine.tec.di.scope.TypedScope;
import jmine.tec.services.annotations.Execution;
import jmine.tec.services.annotations.Output;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;

/**
 * Descreve as saidas de um servico
 * 
 * @author MarceloT
 */
class ServiceOutputDescriber {

    /**
     * Implementa {@link ServiceOutputExtractor}
     */
    private static final class ServiceOutputExtractorImpl implements ServiceOutputExtractor {
        private final List<Method> selectedMethods;

        private final Class<?> declaringClass;

        /**
         * @param selectedMethods {@link List} de {@link Method}
         * @param declaringClass the declaring class
         */
        private ServiceOutputExtractorImpl(List<Method> selectedMethods, Class<?> declaringClass) {
            this.selectedMethods = selectedMethods;
            for (Method method2 : selectedMethods) {
                method2.setAccessible(true);
            }
            this.declaringClass = declaringClass;
        }

        /**
         * {@inheritDoc}
         */
        public Map<String, Object> getOutput(Object target, TypedScope scope) {
            Map<String, Object> map = new HashMap<String, Object>(this.selectedMethods.size());
            for (Method method : this.selectedMethods) {
                Output out = method.getAnnotation(Output.class);
                String name = out.propertyName();
                String refName;
                if (out.literal()) {
                    refName = name;
                } else {
                    refName = scope.get(name, String.class);
                }
                if (refName != null) {
                    try {
                        Object toOut = method.invoke(target);
                        map.put(refName, toOut);
                    } catch (IllegalAccessException e) {
                        throw new IllegalArgumentException(e);
                    } catch (InvocationTargetException e) {
                        MethodBasedInjector.rethrowOrWrap(e);
                    }
                }
            }
            return map;
        }

        /**
         * {@inheritDoc}
         */
        public Map<String, Class<?>> getOutputTypes() {
            Map<String, Class<?>> map = new HashMap<String, Class<?>>(this.selectedMethods.size());
            for (Method method : this.selectedMethods) {
                Output out = method.getAnnotation(Output.class);
                String name = out.propertyName();
                Type type = method.getGenericReturnType();
                if (type instanceof Class<?>) {
                    map.put(name, (Class<?>) type);
                } else if (type instanceof TypeVariable<?> && GenericTypeResolverImpl.getInstance().isResolvable(type, this.declaringClass)) {
                    Class<?> type2 =
                            GenericTypeResolverImpl.getInstance().resolveTypeForFixedGenericType((TypeVariable<?>) type,
                                    this.declaringClass);
                    map.put(name, type2);
                } else {
                    map.put(name, method.getReturnType());
                }
            }
            return map;
        }
    }

    /**
     * Recupera todos os métodos da classe que são {@link Output} e não são {@link Execution}.
     * 
     * @param type {@link Class}
     * @return {@link ServiceOutputExtractor}
     */
    public ServiceOutputExtractor createExtractorFor(Class<?> type) {
        Method[] methods = type.getMethods();
        final List<Method> selectedMethods = new ArrayList<Method>();
        for (Method method : methods) {
            if (!method.isAnnotationPresent(Execution.class) && method.isAnnotationPresent(Output.class)
                    && method.getParameterTypes().length == 0 && method.getReturnType() != void.class
                    && !Modifier.isStatic(method.getModifiers())) {
                selectedMethods.add(method);
            }
        }
        return new ServiceOutputExtractorImpl(selectedMethods, type);
    }

}
